#!/usr/bin/python

# Common framework for any XMPP bot
# Copyright (C) 2009  Denis Nelubin aka Gelin
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

# http://gelin.ru
# mailto:den@gelin.ru
# xmpp:gelin@jabber.ru?message

""" This is the common framework to easy create any XMPP bot.
"""

import sys
import os.path
import logging
import xmpp
import ConfigParser

DEFAULT_CONFIG = '~/.xmpp_bot'
DEFAULT_PORT = 5222
DEFAULT_SECURE = None
DEFAULT_RESOURCE = None

class Options:
    """ Configuration options.
    """
    def __init__(self, configFile=DEFAULT_CONFIG):
        #self._log = logging.getLogger("Options")
        parser = ConfigParser.ConfigParser()
        parser.read(os.path.expanduser(configFile))
        if parser.has_option('account', 'user'):
            self.user = parser.get('account', 'user')
        if parser.has_option('account', 'password'):
            self.password = parser.get('account', 'password')
        if parser.has_option('account', 'server'):
            self.server = parser.get('account', 'server')
        if parser.has_option('account', 'port'):
            self.port = parser.getint('account', 'port')
        if parser.has_option('account', 'resource'):
            self.resource = parser.get('account', 'resource')
        if parser.has_option('account', 'secure'):
            self.secure = parser.getint('account', 'secure')
        if parser.has_option('account', 'jid'):
            (node, domain, resource) = self._getParamsFromJid(
                    parser.get('account', 'jid'))
            if not hasattr(self, 'user'):
                self.user = node
            if not hasattr(self, 'server'):
                self.server = domain
            if not hasattr(self, 'resource') and resource != '':
                self.resource = resource
        if  not hasattr(self, 'port'):
            self.port = DEFAULT_PORT
        if  not hasattr(self, 'resource'):
            self.resource = DEFAULT_RESOURCE
        if  not hasattr(self, 'secure'):
            self.secure = DEFAULT_SECURE
        self.proxy = None   #TODO: add proxy support

    def _getParamsFromJid(self, jid):
        """ Parses JID to the tuple of (node, domain, resource)
        """
        jid1 = xmpp.JID(jid)
        return (jid1.getNode(), jid1.getDomain(), jid1.getResource())


class XmppBot:
    """ Connects to Jabber server and serves user's requests.
    """

    def __init__(self, options):
        """ options parameter is a configuration options.
        """
        self._log = logging.getLogger('XMPPBot')
        self.options = options
        self._conferences = {}
        self._onPresenceHandlers = []
        self._onMessageHandlers = []
        self._onSubsribeHandlers = []
        self._onUserMessageHandlers = []
        self._onMucJoinHandlers = []
        self._onMucJoinFailedHandlers = []
        self._onMucMessageHandlers = []

    def register(self, handler):
        """ Registers new handler
        """
        if callable(getattr(handler, 'onPresence', None)):
            self._onPresenceHandlers.append(handler)
        if callable(getattr(handler, 'onMessage', None)):
            self._onMessageHandlers.append(handler)
        if callable(getattr(handler, 'onSubscribe', None)):
            self._onSubsribeHandlers.append(handler)
        if callable(getattr(handler, 'onUserMessage', None)):
            self._onUserMessageHandlers.append(handler)
        if callable(getattr(handler, 'onMucJoin', None)):
            self._onMucJoinHandlers.append(handler)
        if callable(getattr(handler, 'onMucJoinFailed', None)):
            self._onMucJoinFailedHandlers.append(handler)
        if callable(getattr(handler, 'onMucMessage', None)):
            self._onMucMessageHandlers.append(handler)

    def connect(self):
        """ Connects to and authenticates on jabber server.
            Returns False if connection is failed.
        """
        self._log.info('connecting to %s:%s',
                self.options.server, self.options.port)
        self.connection = xmpp.Client(
                self.options.server, self.options.port, debug=[])
        if self.options.proxy:
            self._log.info('using proxy %s', self.options.proxy)
        result = self.connection.connect(
                proxy=self.options.proxy, secure=self.options.secure)
        self._log.info('connected via %s', result)
        if result == 'tcp':
            self._log.warn('connection is unsecure')
        elif not result:
            self._log.fatal('connection failed')
            return False
        self._log.info('authenticating as %s', self.options.user)
        result = self.connection.auth(
                self.options.user, self.options.password,
                resource=self.options.resource)
        if not result:
            self._log.fatal('authentication failed')
            return False
        self._log.debug('sending initial presence')
        self.connection.sendInitPresence()
        self._log.info('connected successfully')
        self._log.debug('getting roster')
        self.connection.getRoster()
        self.connection.Process(1)  #wait for roster receiving
        self.connection.RegisterHandler('message', self._onMessage)
        self.connection.RegisterHandler('presence', self._onPresence)
        return True

    def serve(self):
        """ Starts serving cycle. Answers to incoming messages.
        """
        try:
            while True:
                self.connection.Process(1)
        except KeyboardInterrupt:
            self._log.info('keyboard termination')

    def _onMessage(self, connection, message):
        """ On message received handler
        """
        for handler in self._onMessageHandlers:
            try:
                handler.onMessage(message, self)
            except BaseException, e:
                self._log.warn('%s onMessage() failed: %s' % (handler, e))
        jid = message.getFrom()
        conf = self._getInvitation(message)
        if (conf):
            #invitation to conference received
            self.joinMuc(conf)
        elif (jid.getResource() and message.getType() in ('chat', None)):
            #only messages from persons and only chats (no errors or groupchat)
            self._onUserMessage(message)
        elif message.getType() == 'groupchat':
            #message from conference
            if self.isJoined(jid):
                self._onMucMessage(message)
            else:
                self._onMucJoin(jid)

    def _onPresence(self, connection, presence):
        """ On presence received handler
        """
        for handler in self._onPresenceHandlers:
            try:
                handler.onPresence(presence, self)
            except BaseException, e:
                self._log.warn('%s onPresence() failed: %s' % (handler, e))
        jid = presence.getFrom()
        ptype = presence.getType()
        if ptype == 'subscribe':
            self._log.info('approving subscription to %s', jid)
            reply = xmpp.Presence(to=jid, typ='subscribed')
            self.connection.send(reply)
            self._log.info('request subscription from %s', jid)
            reply = xmpp.Presence(to=jid, typ='subscribe')
            self.connection.send(reply)
            self._onSubsribe(jid)
        elif ptype == 'subscribed':
            self._log.info('approved subscription from %s', jid)
        elif ptype in ('error', None):
            #check room entering
            confJid = getStripped(jid)
            if self._isJoining(confJid):
                if ptype == 'error':
                    self._log.warn('failed to join to %s: %s', confJid, presence.getError())
                    self._onMucJoinFailed(confJid, presence.getError())
                elif not self.isJoined(confJid):
                    self._onMucJoin(confJid)

    def joinMuc(self, jid):
        """ Joins to specified conference
        """
        jid = xmpp.JID(jid)
        if not jid.getResource():
            jid.setResource(self.options.resource)
        if self.isJoined(jid):
            return
        self._conferences[getStripped(jid)] = 'joining'
        self._log.info('joining to %s', jid)
        presence = xmpp.Presence(to=jid)
        self.connection.send(presence)

    def _isJoining(self, jid):
        """ Checks is we are joining to specified conference
        """
        confJid = getStripped(jid)
        if not self._conferences.has_key(confJid):
            return False
        if self._conferences[confJid] == 'joining':
            return True
        return False

    def isJoined(self, jid):
        """ Checks is we were joined to specified conference
        """
        confJid = getStripped(jid)
        if not self._conferences.has_key(confJid):
            return False
        if self._conferences[confJid] == 'joined':
            return True
        return False

    def _getInvitation(self, message):
        """ Checks if specified message is inivitation to groupchat and
            returns room JID or None
        """
        result = None
        #check as groupchat
        node = message.getTag('x', namespace='jabber:x:conference')
        if node:
            result = node.getAttr('jid')
        else:
            #check as MUC
            node = message.getTag('x', namespace='http://jabber.org/protocol/muc#user')
            if node and node.getTag('invite'):
                result = message.getFrom()
        if result:
            self._log.info('got invitation to %s', result)
        return result

    def _onSubsribe(self, jid):
        """ Called internally when presence subscription is processed
        """
        for handler in self._onSubsribeHandlers:
            try:
                handler.onSubscribe(jid, self)
            except BaseException, e:
                self._log.warn('%s onSubscribe() failed: %s' % (handler, e))

    def _onUserMessage(self, message):
        """ Called internally when user message (not MUC message,
            not system message) is received
        """
        for handler in self._onUserMessageHandlers:
            try:
                handler.onUserMessage(message, self)
            except BaseException, e:
                self._log.warn('%s onUserMessage() failed: %s' % (handler, e))

    def _onMucJoin(self, jid):
        """ Called internally when successfully joined to conference
        """
        confJid = getStripped(jid)
        self._log.info('joined to %s', confJid)
        self._conferences[confJid] = 'joined'
        for handler in self._onMucJoinHandlers:
            try:
                handler.onMucJoin(confJid, self)
            except BaseException, e:
                self._log.warn('%s onMucJoin() failed: %s' % (handler, e))

    def _onMucJoinFailed(self, jid, error):
        """ Called internally when joining to conference failed
        """
        confJid = getStripped(jid)
        del self._conferences[confJid]
        for handler in self._onMucJoinFailedHandlers:
            try:
                handler.onMucJoinFailed(confJid, error, self)
            except BaseException, e:
                self._log.warn('%s onMucJoinFailed() failed: %s' % (handler, e))

    def _onMucMessage(self, message):
        """ Called internally when MUC message is received
        """
        for handler in self._onMucMessageHandlers:
            try:
                handler.onMucMessage(message, self)
            except BaseException, e:
                self._log.warn('%s onMucMessage() failed: %s' % (handler, e))

class DebugHandler:
    """ Sample handler which just prints the event cause
        to the log.
    """

    def __init__(self):
        self._log = logging.getLogger('DebugHandler')

    def onPresence(self, presence, bot):
        self._log.debug('presence: %s' % presence)

    def onMessage(self, message, bot):
        self._log.debug('message: %s' % message)

    def onSubscribe(self, jid, bot):
        self._log.debug('subscribe: %s' % jid)

    def onUserMessage(self, message, bot):
        self._log.debug('user message: %s' % message)

    def onMucJoin(self, jid, bot):
        self._log.debug('joined: %s' % jid)

    def onMucJoinFailed(self, jid, error, bot):
        self._log.debug('join failed: %s %s' % (jid, error))

    def onMucMessage(self, message, bot):
        self._log.debug('muc message: %s' % message)

def makeReplyMessage(message):
    """ Constructs reply message
    """
    #message = message.buildReply(reply)    #doesn't copy message type
    reply = xmpp.Message(to=message.getFrom(),
                         typ=message.getType(),
                         subject=message.getSubject(),
                         frm=message.getTo())
    thread = message.getThread()
    if thread:
        reply.setThread(message.getThread())
    return reply

def makeFullJid(bareJid, resource):
    """ Makes full JID from provided bare JID and resource
    """
    jid = xmpp.JID(bareJid)
    jid.setResource(resource)
    return jid

def getStripped(jid):
    """ Makes JID without resource
    """
    # xmpp.JID.getStripped() returns string, not JID object
    return xmpp.JID(node=jid.getNode(), domain=jid.getDomain())

if __name__ == '__main__':
    #logging configuration
    logLevel = os.environ.get("LOG_LEVEL", "DEBUG").upper()
    logging.basicConfig(level=getattr(logging, logLevel),
                    format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S')
    #get options
    if len(sys.argv) > 1:
        options = Options(sys.argv[1])
    else:
        options = Options()
    #create bot
    bot = XmppBot(options)
    bot.register(DebugHandler())
    #start bot
    if not bot.connect():
        sys.exit(1)
    bot.serve()